Skip to content

HigherOrderGraph / modified MultiOrderModel - #329

Open
vineetbansal wants to merge 21 commits into
devfrom
vb/hograph
Open

vineetbansal wants to merge 21 commits into
devfrom
vb/hograph

Conversation

@vineetbansal

Copy link
Copy Markdown
Collaborator

A HigherOrderGraph class of arbitrary order, constructable from temporal_graph/event_graph/path_data. It delegates most of the work to the MultiOrderModel class which does the iterations on the lifting (with cached=False). It is also constructable using from_aggregated, and has its own .lift method.

The MultiOrderModel class now has HigherOrderGraphs in its layers.

None of the tests for MultiOrderModel needed modifications and still pass, which is reassuring. Tests for HigherOrderModel and MultiOrderModel that assume HigherOrderModel in layers are coming next.

This PR assumes that the EventGraph branch is merged, as it builds on top of it.

Typical workflow using these new classes:

def data() -> pp.TemporalGraph:
    r"""

        a           d
          \        /
            c  (hub)
          /        \
        b           e

    """
    return pp.TemporalGraph.from_edge_list(
        [
            ("a", "c", 1), ("c", "d", 2),   # a -> c -> d
            ("b", "c", 3), ("c", "e", 4),   # b -> c -> e
            ("a", "c", 5), ("c", "d", 6),
            ("b", "c", 7), ("c", "e", 8),
        ]
    )

t = data()
DELTA = 1
eg = EventGraph.from_temporal_graph(t, delta=DELTA)
h1 = HigherOrderGraph.from_temporal_graph(t, order = 1)
assert h1.order == 1

h2 = HigherOrderGraph.from_event_graph(eg)
assert h2.order == 2

paths = PathData(IndexMap(list("abcde")))
paths.append_walk(("a", "c", "d"), weight=3)
paths.append_walk(("b", "c", "e"), weight=3)

h1b = HigherOrderGraph.from_path_data(paths, order = 1)
h1c = HigherOrderGraph.from_event_graph(eg, order = 2)

h5 = HigherOrderGraph.from_event_graph(eg, order=5)  # Create order 5 ho (but still has to go through 2->5 algorithmically)

print("\n=== HigherOrderGraph (order 2) ===")
print("order:", h2.order)                   # 2
print("nodes:", h2.nodes)                   # [('a','c'), ('b','c'), ('c','d'), ('c','e')]
print("edges:", h2.edges)                   # [(('a','c'),('c','d')), (('b','c'),('c','e'))]
print("weights:", h2.data.edge_weight)      # [2., 2.]

assert h2.order == 2
assert h2.n == 4                            # 8 events collapsed into 4 nodes
assert h2.n_first_order == 5
assert h2.first_order_mapping.to_id(0) == "a"

h3 = h2.lift()
assert isinstance(h3, HigherOrderGraph)
assert h3.order == 3
print("order-3 nodes:", h3.nodes)  # [('a', 'c', 'd'), ('b', 'c', 'e')]


MAX_ORDER = 2

# build MultiOrderModel from TemporalGraph
m = MultiOrderModel.from_temporal_graph(t, delta=DELTA, max_order=MAX_ORDER)

for k, layer in sorted(m.layers.items()):
    print(f"  layer {k}: order={layer.order}  n={layer.n}  m={layer.m}")
    #   layer 1: order=1  n=5  m=4
    #   layer 2: order=2  n=4  m=2
    assert isinstance(layer, HigherOrderGraph)
    assert layer.order == k
    assert layer.n_first_order == t.n

# build MultiOrderModel from EventGraph
m_via_eg = MultiOrderModel.from_event_graph(eg, max_order=MAX_ORDER)
assert m_via_eg.layers[2].edges == m.layers[2].edges

# build MultiOrderModel from PathData
paths = pp.PathData(pp.IndexMap(list("abcde")))
paths.append_walks(node_seqs=[("a", "c", "d"), ("b", "c", "e")], weights=[4, 4])
m_paths = MultiOrderModel.from_path_data(paths, max_order=MAX_ORDER)

@vineetbansal vineetbansal changed the title Vb/hograph HigherOrderGraph / modified MultiOrderModel Aug 13, 2026

@M-Lampert M-Lampert left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR looks good already. I made some comments; let me know if you have any questions. I think some of the questions that I raised should be discussed together with the others in our next meeting. I said so in the comments as well.

Comment thread src/pathpyG/core/higher_order_graph.py Outdated
Comment thread src/pathpyG/core/higher_order_graph.py Outdated
Comment thread src/pathpyG/core/higher_order_graph.py Outdated
Comment thread src/pathpyG/core/multi_order_model.py Outdated
Comment thread src/pathpyG/core/graph.py Outdated
Comment thread src/pathpyG/core/graph.py Outdated
Comment thread src/pathpyG/core/graph.py Outdated
Comment thread src/pathpyG/core/higher_order_graph.py
@vineetbansal

vineetbansal commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Note from 09/10 meeting - okay to restrict Graph to order 1, and use HigherOrderGraph for the more general case, any order >= 0.

TODO: Vineet - add test cases for HigherOrderGraph as a start. @M-Lampert can then add test cases for order=0.

@vineetbansal

vineetbansal commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator Author

@M-Lampert - since you reviewed this last, I've incorporated the changes we discussed during our meeting.

  • The HigherOrderGraph class now represents the observed, weighted De Bruijn graph of any order k >= 0 and owns node_sequence. It has constructors from path data, temporal graphs, event graphs, plain graphs (order 1) and node weights (order 0). Order 0 is a single empty-path node () with one weighted self-loop per first-order node.
  • Graph is always first-order. It no longer carries a node_sequence and order is always 1. It rejects data that carries a node_sequence - this is a breaking change
  • EventGraph now manages its own node_sequence. EventGraph.order is now 1. Before, it was 2 as a side effect of its node_sequence.
  • MultiOrderModel:
    • its layers are HigherOrderGraphs;
    • from_temporal_graph and from_event_graph reject max_order=0
    • from_path_data always keeps layers 0 and 1, even with cached=False
  • algorithms/lift_order.py:
    • aggregate_edge_index returns raw Data.
  • Removed: HigherOrderGraph.lift. from_aggregated was renamed to aggregate.

@vineetbansal

vineetbansal commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator Author

@M-Lampert - with these changes I'm realizing that the order attribute only makes sense for a HigherOrderGraph (where it does exist) and not for Graph, TemporalGraph, or EventGraph (where it exists, is set to 1, but is unused by the rest of the code), so it can perhaps be removed altogether.

Let's discuss this tomorrow.

From Moritz - okay to remove order from these 3 classes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants